import {
  ALL,
  Config,
  httpError,
  Inject,
  MidwayConfig,
  Provide
} from '@midwayjs/core'
import { JwtService } from '@midwayjs/jwt'
import { Context } from '@midwayjs/koa'
import { InjectEntityModel } from '@midwayjs/typeorm'
import { CacheManager } from '@ttou/midway-cache'
import { Repository } from 'typeorm'

import { APP_MESSAGES } from '@/constant'
import { SysLoginInfoEntity, SysUserEntity } from '@/entity'
import {
  AddressService,
  ConfigService,
  IpService,
  StateService,
  UaService
} from '@/service'
import { CacheKeyUtil, IdUtil, PasswordUtil } from '@/util'

import { LoginDTO } from './auth.dto'

@Provide()
export class AuthService {
  @Config(ALL)
  midwayConfig: MidwayConfig

  @Inject()
  ctx: Context

  @Inject()
  jwtService: JwtService

  @Inject()
  configService: ConfigService

  @Inject()
  cacheManager: CacheManager

  @InjectEntityModel(SysUserEntity)
  userModel: Repository<SysUserEntity>

  @InjectEntityModel(SysLoginInfoEntity)
  loginInfoModel: Repository<SysLoginInfoEntity>

  async handleLogin(dto: LoginDTO) {
    const ipService = await this.ctx.requestContext.getAsync(IpService)

    const { userName, password } = dto
    const user = await this.userModel.findOne({
      relations: {
        dept: true
      },
      select: {
        dept: {
          deptId: true,
          deptName: true
        }
      },
      where: {
        userName,
        delFlag: '0'
      }
    })

    if (user) {
      const isMatch = PasswordUtil.compare(password, user.password)

      const deptName = user.dept ? user.dept.deptName : ''
      const loginDate = new Date()
      const loginIp = ipService.getByCtx()
      const tokenId = IdUtil.uuidV4()
      const userId = user.userId

      if (isMatch) {
        user.loginDate = loginDate
        user.loginIp = loginIp
        await this.userModel.save(user)

        const { os, browser, loginLocation } = await this.#saveLoginInfo({
          tokenId,
          userName,
          status: '0',
          msg: APP_MESSAGES.LOGIN_SUCCESS,
          loginDate,
          loginIp
        })

        await this.#saveToken({
          tokenId,
          userId,
          userName,
          deptName,
          os,
          browser,
          loginLocation,
          loginDate,
          loginIp
        })

        return this.jwtService.sign({ userName, sub: tokenId })
      }

      await this.#saveLoginInfo({
        userName,
        status: '1',
        msg: APP_MESSAGES.ACCOUNT_OR_PASSWORD_ERROR,
        loginDate,
        loginIp
      })

      this.ctx.throw(
        new httpError.BadRequestError(APP_MESSAGES.ACCOUNT_OR_PASSWORD_ERROR)
      )
    }

    this.ctx.throw(new httpError.BadRequestError(APP_MESSAGES.USER_NOT_EXIST))
  }

  async handleLogout() {
    const stateService = await this.ctx.requestContext.getAsync(StateService)
    const isSoloLogin = await this.configService.isSoloLogin()

    const jwtPayload = stateService.getJwtPayload()

    // 判断 token 是否已过期
    if (jwtPayload) {
      const tokenKey = CacheKeyUtil.getTokenKey(jwtPayload.sub)

      if (isSoloLogin) {
        const { userId } = await this.cacheManager.get<IOnlineInfo>(tokenKey)

        const userKey = CacheKeyUtil.getUserKey(userId)

        await this.cacheManager.del(userKey)
      }

      await this.cacheManager.del(tokenKey)
    }
  }

  async #saveToken(value: any) {
    const isSoloLogin = await this.configService.isSoloLogin()

    const { tokenId, userId, ...rest } = value
    const ttl = this.midwayConfig.jwt.expiresIn as number

    if (isSoloLogin) {
      const userKey = CacheKeyUtil.getUserKey(userId)

      const oldTokenId = await this.cacheManager.get<string>(userKey)

      if (oldTokenId) {
        await this.cacheManager.del(CacheKeyUtil.getTokenKey(oldTokenId))
      }

      await this.cacheManager.set(userKey, tokenId, ttl)
    }

    await this.cacheManager.set(
      CacheKeyUtil.getTokenKey(tokenId),
      {
        tokenId,
        userId,
        ...rest
      },
      ttl
    )
  }

  async #saveLoginInfo(value: ILoginInfo) {
    const addressService = await this.ctx.requestContext.getAsync(
      AddressService
    )
    const uaService = await this.ctx.requestContext.getAsync(UaService)

    const ua = uaService.getByCtx()
    const loginLocation = await addressService.getByIp()
    const os = ua.os.name
    const browser = ua.browser.name

    const loginInfo = new SysLoginInfoEntity()

    Object.assign<any, Partial<SysLoginInfoEntity>>(loginInfo, {
      os,
      browser,
      loginLocation,
      ...value
    })

    await this.loginInfoModel.save(loginInfo)

    return {
      os,
      browser,
      loginLocation
    }
  }
}
